테스트 자동화는 Grails의 핵심 기능중 하나이고 Groovy Test를 이용하여 구현됐다. 그래서 Grails에서는 저 수준 유닛 테스트부터 고 수준 기능 테스트까지 쉽게 작성할 수 있다. 이 절에서는 테스트를 위해 Grails가 지원하는 것들에 대해 설명한다.먼저 모든 create-* 명령어들은 명령을 마칠 때 자동으로 "통합 테스트(integration test)"를 생성한다는 것을 알아야 한다. 예를 들어 다음과 같이 create-controller 명령을 실행하면:grails create-controller simple
Grails는 grails-app/controllers/SimpleController.groovy에 컨트롤러를 생성할 뿐만 아니라 test/integration/SimpleControllerTests.groovy에 통합 테스트도 생성한다. 하지만 Grails가 테스트 로직까지 채워주지 않기 때문에 직접 해야 한다.이렇게 했다면 모든 테스트를 test-app 명령어로 테스트를 실행할 수 있다:이 명령어를 실행하면 다음과 같이 출력된다:-------------------------------------------------------
Running Unit Tests…
Running test FooTests...FAILURE
Unit Tests Completed in 464ms …
-------------------------------------------------------Tests failed: 0 errors, 1 failures
테스트 보고서는 test/reports 디렉토리에 저장될 것이다. 그리고 테스트 이름을 명시해서 테스트 별로 실행할 수도 있다(이때 테스트 접미어는 생략한다):grails test-app SimpleController
스페이스를 구분자로 테스트의 이름들을 나열하는 식으로 여러 테스트를 한꺼번에 실행시킬 수도 있다:grails test-app SimpleController BookController
유닛 테스팅은 "유닛" 수준에서 수행하는 테스트를 말한다. 즉, 기반 구조에 대한 고려없이 각각의 메소드나 코드 블럭을 테스트하는 것이다. Grails의 유닛 테스트와 통합 테스트의 차이점을 알아야 한다. Grails는 런타임이나 통합 테스트를 할 때처럼 유닛 테스트에 어떠한 동적 메소드도 주입하지 않는다.This makes sense if you consider that the methods injected by Grails typically community with the database (with GORM) or the underlying Servlet engine (with Controllers).Grails가 데이터베이스(GORM)와 통신하기 위해 주입하는 메소드나 서블릿 엔진에 의해 컨트롤러에 주입되는 메소드와 함께 이해할 수 있다. 예를 들어 BookController
에 다음과 같은 액션이 있다고 하면:class MyService {
def otherService String createSomething() {
def stringId = otherService.newIdentifier()
def item = new Item(code: stringId, name: "Bangle")
item.save()
return stringId
} int countItems(String name) {
def items = Item.findAllByName(name)
return items.size()
}
}
As you can see the service takes advantage of GORM methods. So how do you go about testing the above code in a unit test? The answer can be found in Grails' testing support classes.The Testing Framework
The core of the testing plugin is the grails.test.GrailsUnitTestCase
class. This is a sub-class of GroovyTestCase
geared towards Grails applications and their artifacts. It provides several methods for mocking particular types as well as support for general mocking a la Groovy's MockFor and StubFor classes.Normally you might look at the MyService
example shown previously and the dependency on another service and the use of dynamic domain class methods with a bit of a groan. You can use meta-class programming and the "map as object" idiom, but these can quickly get ugly. How might we write the test with GrailsUnitTestCase ?import grails.test.GrailsUnitTestCaseclass MyServiceTests extends GrailsUnitTestCase {
void testCreateSomething() {
// Mock the domain class.
def testInstances = []
mockDomain(Item, testInstances) // Mock the "other" service.
String testId = "NH-12347686"
def otherControl = mockFor(OtherService)
otherControl.demand.newIdentifier(1..1) {-> return testId } // Initialise the service and test the target method.
def testService = new MyService()
testService.otherService = otherControl.createMock() def retval = testService.createSomething() // Check that the method returns the identifier returned by the
// mock "other" service and also that a new Item instance has
// been saved.
assertEquals testId, retval
assertEquals 1, testInstances
assertTrue testInstances[0] instanceof Item
} void testCountItems() {
// Mock the domain class, this time providing a list of test
// Item instances that can be searched.
def testInstances = [ new Item(code: "NH-4273997", name: "Laptop"),
new Item(code: "EC-4395734", name: "Lamp"),
new Item(code: "TF-4927324", name: "Laptop") ]
mockDomain(Item, testInstances) // Initialise the service and test the target method. def testService = new MyService() assertEquals 2, testService.countItems("Laptop")
assertEquals 1, testService.countItems("Lamp")
assertEquals 0, testService.countItems("Chair")
}
}
OK, so a fair bit of new stuff there, but once we break it down you should quickly see how easy it is to use the methods available to you. Take a look at the "testCreateSomething()" test method. The first thing you will probably notice is the mockDomain()
method, which is one of several provided by GrailsUnitTestCase
:def testInstances = []
mockDomain(Item, testInstances)
It adds all the common domain methods (both instance and static) to the given class so that any code using it sees it as a full-blown domain class. So for example, once the Item
class has been mocked, we can safely call the save()
method on instances of it. Speaking of which, what happens when we call that method on a mocked domain class? Simple: the new instance is added to the testInstances
list we passed into the mockDomain()
method.The next bit we want to look at is centered on the mockFor
method:def otherControl = mockFor(OtherService)
otherControl.demand.newIdentifier(1..1) {-> return testId }
This is analagous to the MockFor
and StubFor
classes that come with Groovy and it can be used to mock any class you want. In fact, the "demand" syntax is identical to that used by Mock/StubFor, so you should feel right at home. Of course you often need to inject a mock instance as a dependency, but that is pretty straight forward with the createMock()
method, which you simply call on the mock control as shown. For those familiar with EasyMock, the name otherControl
highlights the role of the object returned by mockFor()
- it is a control object rather than the mock itself.The rest of the testCreateSomething()
method should be pretty familiar, particularly as you now know that the mock save()
method adds instances to testInstances
list. However, there is an important technique missing from the test method. We can determine that the mock newIdentifier()
method is called because its return value has a direct impact on the result of the createSomething()
method. But what if that weren't the case? How would we know whether it had been called or not? With Mock/StubFor the check would be performed at the end of the use()
closure, but that's not available here. Instead, you can call verify()
on the control object - in this case otherControl
. This will perform the check and throw an assertion error if it hasn't been called when it should have been.Lastly, testCountItems()
in the example demonstrates another facet of the mockDomain()
method:def testInstances = [ new Item(code: "NH-4273997", name: "Laptop"),
new Item(code: "EC-4395734", name: "Lamp"),
new Item(code: "TF-4927324", name: "Laptop") ]
mockDomain(Item, testInstances)
It is normally quite fiddly to mock the dynamic finders manually, and you often have to set up different data sets for each invocation. On top of that, if you decide a different finder should be used then you have to update the tests to check for the new method! Thankfully the mockDomain()
method provides a lightweight implementation of the dynamic finders backed by a list of domain instances. Simply provide the test data as the second argument of the method and the mock finders will just work.GrailsUnitTestCase - the mock methods
You have already seen a couple of examples in the introduction of the mock..()
methods provided by the GrailsUnitTestCase
class. Here we will look at all the available methods in some detail, starting with the all-purpose mockFor()
. But before we do, there is a very important point to make: using these methods ensures that any changes you make to the given classes do not leak into other tests! This is a common and serious problem when you try to perform the mocking yourself via meta-class programming, but that headache just disappears as long as you use at least one of mock..()
methods on each class you want to mock.mockFor(class, loose = false)
General purpose mocking that allows you to set up either strict or loose demands on a class.This method is surprisingly intuitive to use. By default it will create a strict mock control object (one for which the order in which methods are called is important) that you can use to specify demands:def strictControl = mockFor(MyService)
strictControl.demand.someMethod(0..2) { String arg1, int arg2 -> … }
strictControl.demand.static.aStaticMethod {-> … }
Notice that you can mock static methods as well as instance ones simply by using the "static" property after "demand". You then specify the name of the method that you want to mock with an optional range as its argument. This range determines how many times you expect the method to be called, so if the number of invocations falls outside of that range (either too few or too many) then an assertion error will be thrown. If no range is specified, a default of "1..1" is assumed, i.e. that the method must be called exactly once.The last part of a demand is a closure representing the implementation of the mock method. The closure arguments should match the number and types of the mocked method, but otherwise you are free to add whatever you want in the body.As we mentioned before, if you want an actual mock instance of the class that you are mocking, then you need to call mockControl.createMock()
. In fact, you can call this as many times as you like to create as many mock instances as you need. And once you have executed the test method, you can call mockControl.verify()
to check whether the expected methods were actually called or not.Lastly, the call:def looseControl = mockFor(MyService, true)
will create a mock control object that has only loose expectations, i.e. the order that methods are invoked does not matter.mockDomain(class, testInstances = )
Takes a class and makes mock implementations of all the domain class methods (both instance- and static-level) accessible on it.Mocking domain classes is one of the big wins from using the testing plugin. Manually doing it is fiddly at best, so it's great that mockDomain() takes that burden off your shoulders.In effect, mockDomain()
provides a lightweight version of domain classes in which the "database" is simply a list of domain instances held in memory. All the mocked methods ( save()
, get()
, findBy*()
, etc.) work against that list, generally behaving as you would expect them to. In addition to that, both the mocked save()
and validate() methods will perform real validation (support for the unique constraint included!) and populate an errors object on the corresponding domain instance.There isn't much else to say other than that the plugin does not support the mocking of criteria or HQL queries. If you use either of those, simply mock the corresponding methods manually (for example with mockFor()
) or use an integration test with real data.mockForConstraintsTests(class, testInstances = )
Highly specialised mocking for domain classes and command objects that allows you to check whether the constraints are behaving as you expect them to.Do you test your domain constraints? If not, why not? If your answer is that they don't need testing, think again. Your constraints contain logic and that logic is highly susceptible to bugs - the kind of bugs that can be tricky to track down (particularly as save() doesn't throw an exception when it fails). If your answer is that it's too hard or fiddly, that is no longer an excuse. Enter the mockForConstraintsTests()
method.This is like a much reduced version of the mockDomain()
method that simply adds a validate()
method to a given domain class. All you have to do is mock the class, create an instance with field values, and then call validate()
. You can then access the errors property on your domain instance to find out whether the validation failed or not. So if all we are doing is mocking the validate()
method, why the optional list of test instances? That is so that we can test unique constraints as you will soon see.So, suppose we have a simple domain class like so:class Book {
String title
String author static constraints = {
title(blank: false, unique: true)
author(blank: false, minSize: 5)
}
}
Don't worry about whether the constraints are sensible or not (they're not!), they are for demonstration only. To test these constraints we can do the following:class BookTests extends GrailsUnitTestCase {
void testConstraints() {
def existingBook = new Book(title: "Misery", author: "Stephen King")
mockForConstraintsTests(Book, [ existingBook ]) // Validation should fail if both properties are null.
def book = new Book()
assertFalse book.validate()
assertEquals "nullable", book.errors["title"]
assertEquals "nullable", book.errors["author"] // So let's demonstrate the unique and minSize constraints.
book = new Book(title: "Misery", author: "JK")
assertFalse book.validate()
assertEquals "unique", book.errors["title"]
assertEquals "minSize", book.errors["author"] // Validation should pass!
book = new Book(title: "The Shining", author: "Stephen King")
assertTrue book.validate()
}
}
You can probably look at that code and work out what's happening without any further explanation. The one thing we will explain is the way the errors property is used. First, it does return a real Spring Errors
instance, so you can access all the properties and methods you would normally expect. Second, this particular Errors
object also has map/property access as shown. Simply specify the name of the field you are interested in and the map/property access will return the name of the constraint that was violated. Note that it is the constraint name , not the message code (as you might expect).That's it for testing constraints. One final thing we would like to say is that testing the constraints in this way catches a common error: typos in the "constraints" property! It is currently one of the hardest bugs to track down normally, and yet a unit test for your constraints will highlight the problem straight away.mockLogging(class, enableDebug = false)
Adds a mock "log" property to a class. Any messages passed to the mock logger are echoed to the console.mockController(class)
Adds mock versions of the dynamic controller properties and methods to the given class. This is typically used in conjunction with the ControllerUnitTestCase
class.mockTagLib(class)
Adds mock versions of the dynamic taglib properties and methods to the given class. This is typically used in conjunction with the TagLibUnitTestCase
class.
통합 테스트는 모든 코드를 테스트할 수 있어야 한다는 점에서 유닛 테스트와 다르다 Grails는 통합 테스트에 HSQLDB라는 메모리 데이터베이스를 사용하고 각 테스트마다 데이터베이스의 모든 데이터를 삭제할 것이다.Testing Controllers(컨트롤러 테스트하기)
컨트롤러를 테스트하려면 먼저 스프링 Mock 라이브러리를 이해해야 한다.Grails의 테스트에서는 내부적으로 MockHttpServletRequest, MockHttpServletResponse, MockHttpSession을 사용한다. 다음과 같이 이 것들을 이용해서 테스트할 수 있다:class FooController { def text = {
render "bar"
} def someRedirect = {
redirect(action:"bar")
}
}
다음과 같이 이 코드에 대한 테스트를 만든다:class FooControllerTests extends GroovyTestCase { void testText() {
def fc = new FooController()
fc.text()
assertEquals "bar", fc.response.contentAsString
} void testSomeRedirect() { def fc = new FooController()
fc.someRedirect()
assertEquals "/foo/bar", fc.response.redirectedUrl
}
}
이 예제에서 응답은 MockHttpServletResponse의 인스턴스이다. 응답에 쓴 결과를 contentAsString으로 결과를 확인하거나 리다이렉트된 URL을 얻어오기 위해 사용한다. Servlet API의 Mock 버전은 실제 버전과 달라서 모든 것을 변경할 수 있고 contextPath같은 요청의 속성도 설정할 수 있다.통합 테스트 중에는 액션이 호출될 때 Grails가 인터셉터(interceptors)를 자동으로 실행하지 않는다. 인터셉터를 테스트해야 한다면 인터셉터만 별도로 기능 테스트(functional testing)를 이용해 테스트한다.Testing Controllers with Services(서비스와 함께 컨트롤러 테스트하기)
만약 컨트롤러가 서비스를 참조하고 있다면 테스트에서 서비스를 명시적으로 초기화 시켜야 한다.주어진 컨트롤러가 다음과 같으면:class FilmStarsController {
def popularityService def update = {
// do something with popularityService
}
}
테스트를 다음과 같이 만들 수 있다:class FilmStarsTests extends GroovyTestCase {
def popularityService public void testInjectedServiceInController () {
def fsc = new FilmStarsController()
fsc.popularityService = popularityService
fsc.update()
}
}
Testing Controller Command Objects(컨트롤러의 커맨드 객체 테스트하기)
커맨드 객체로 요청에 파라미터를 제공하는 방법이 있다. 커맨드 객체는 파라미터 없이 액션이 호출되면 자동으로 실행된다:다음과 같이 커맨드 객체를 사용하는 객체가 있을 때:class AuthenticationController {
def signup = { SignupForm form ->
…
}
}
다음과 같이 테스트를 할 수 있다:def controller = new AuthenticationController()
controller.params.login = "marcpalmer"
controller.params.password = "secret"
controller.params.passwordConfirm = "secret"
controller.signup()
Grails는 마법처럼 자동으로 Mock 요청 파라미터들을 커맨드 객체에 할당시켜서 siginup 액션를 실행시킨다. 컨트롤러를 테스트 하는 동안 그레일즈가 제공하는 Mock 요청으로 params가 바뀔 수도 있다.Testing Controllers and the render Method(render 메소드를 사용하는 컨트롤러 테스트하기)
render 메소드를 사용하면 모든 액션에서 원하는 뷰를 렌더링할 수 있다. 예를 들어 다음과 같은 예제가 있을 때:def save = {
def book = Book(params)
if(book.save()) {
// handle
}
else {
render(view:"create", model:[book:book])
}
}
액션 모델의 결과는 반환되지 않지만 대신에 컨트롤러의 modelAndView 속성에 저장된다. modelAndView 속성은 Spring MVC's ModelAndView 클래스의 인스턴스이고 이 속성을 사용하여 액션의 결과를 테스트할 수 있다:def bookController = new BookController()
bookController.save()
def model = bookController.modelAndView.model.book
Simulating Request Data(요청 데이터 시뮬레이션)
액션을 테스트하려면 REST 웹 서비스처럼 어떤 요청 데이터가 필요하다. 이를 위해 스프링의 MockHttpServletRequest 객체를 사용할 수 있다. 예를 들어 다음의 액션은 들어오는 요청을 바인딩한다:def create = {
[book: new Book(params['book']) ]
}
다음과 같이 XML로 book 파라미터를 시뮬레이션할 수 있다:void testCreateWithXML() {
def controller = new BookController()
controller.request.contentType = 'text/xml'
controller.request.contents = '''<?xml version="1.0" encoding="ISO-8859-1"?>
<book>
<title>The Stand</title>
…
</book>
'''.getBytes() // note we need the bytes def model = controller.create()
assert model.book
assertEquals "The Stand", model.book.title
}
JSON 요청에도 동일하게 적용할 수 있다:void testCreateWithJSON() {
def controller = new BookController()
controller.request.contentType = "text/json"
controller.request.content = '{"id":1,"class":"Book","title":"The Stand"}'.getBytes() def model = controller.create()
assert model.book
assertEquals "The Stand", model.book.title}
JSON을 사용할 때에는 class 속성에 바인딩할 객체의 형식을 명시해야 한다는 것을 잊으면 안된다. XML에서는 <book>노드의 이름에서 유추하지만 JSON에서는 JSON 패킷에 이 속성을 명시해야 한다.
REST 웹 서비스에 대한 정보는 REST 절을 보라.Testing Web Flows(웹 플로우 테스트하기)
웹 플로우(Web Flows)를 테스팅하는 것은 grails.test.WebFlowTestCase라는 특별한 테스트 도구harness가 필요하다. 이 클래스는 스프링 웹 플로우에 포함된 AbstractFlowExecutionTests 서브클래스이다.
WebFlowTestCase의 서브클래스는 통합 테스트를 의미한다.
일반적으로 다음과 같은 플로우가 있을 때:class ExampleController {
def exampleFlow = {
start {
on("go") {
flow.hello = "world"
}.to "next"
}
next {
on("back").to "start"
on("go").to "end"
}
end()
}
}
어떤 플로우를 사용할 것인지 테스트 도구(harness)에 알려줘야 한다. 추상 메소드를 getFlow를 오버라이딩해서 테스트 도구에 알릴 수 있다:class ExampleFlowTests extends grails.test.WebFlowTestCase {
def getFlow() { new ExampleController().exampleFlow }
…
}
만약 flow id를 명시해야 한다면 getFlowId 메소드를 오버라이딩해서 명시할 수 있다. 그렇지않으면 기본 값이 사용된다:class ExampleFlowTests extends grails.test.WebFlowTestCase {
String getFlowId() { "example" }
…
}
이 것을 다 끝냈으면 startFlow 메소드로 flow를 시작할 수 있다다. 이 startFlow 메소드는 ViewSelection 객체를 반환한다:void testExampleFlow() {
def viewSelection = startFlow() assertEquals "start", viewSelection.viewName
…
}
이 예제는 viewSelection 객체의 viewName 속성를 이용하여 현재 올바른 상태에 있는지 검사할 수 있다는 것을 보여준다. 그리고 siginalEvent 메소드를 사용하여 event를 발생시킬 수 있다:void testExampleFlow() {
…
viewSelection = signalEvent("go")
assertEquals "next", viewSelection.viewName
assertEquals "world", viewSelection.model.hello
}
여기서 “go” 이벤트를 실행시켜야 한다는 것을 플로우에 알렸다. 그래서 “next” 상태로 전이될 수 있다. 이 예제의 전이 액션은 flow 스콥의 hello 변수에 값을 채운다. 그리고 viewSelection의 model속성을 이용하여 변수의 값이 제대로 적용됐는지 검사할 수 있다.Testing Tag Libraries(태그 라이브러리 테스트하기)
태그가 메소드 처럼 실행될 때 결과를 실질적으로 문자열로 반환하기 때문에 태그 라이브러리를 테스트하는 것은 꽤 쉽다. 만약 다음과 같은 태그 라이브러리가 있다면:class FooTagLib {
def bar = { attrs, body ->
out << "<p>Hello World!</p>"
} def bodyTag = { attrs, body ->
out << "<${attrs.name}>"
out << body()
out << "</${attrs.name}>"
}
}
테스트는 다음과 같이 작성한다:class FooTagLibTests extends GroovyTestCase { void testBarTag() {
assertEquals "<p>Hello World!</p>", new FooTagLib().bar(null,null)
} void testBodyTag() {
assertEquals "<p>Hello World!</p>", new FooTagLib().bodyTag(name:"p") {
"Hello World!"
}
}
}
두 번째 예제인 testBodyTag를 잘 살펴보자. 태그의 바디를 반환하는 블럭을 넘기는데 이 것으로 바디의 내용을 쉽게 문자열로 표현할 수 있다.Testing Tag Libraries with GroovyPagesTestCase(GroovyPagesTestCase로 태그 라이브러리 테스트하기)
위의 예제를 grails.test.GroovyPagesTestCase 클래스를 사용하여 더 쉽게 테스트할 수 있다.GroovyPagesTestCase 클래스는 GroovyTestCase 클래스의 서브클래스이고 GSP 렌더링의 출력을 테스팅할 수 있는 메소드들이 추가되어 있다.
GroovyPagesTestCase는 오직 통합 테스트에서만 사용할 수 있다.
다음과 같이 날짜를 표현하는(formatting) 태그 라이브러리가 있다고 하면:class FormatTagLib {
def dateFormat = { attrs, body ->
out << new java.text.SimpleDateFormat(attrs.format) << attrs.date
}
}
이 것은 다음과 같이 쉽게 태스트할 수 있다:class FormatTagLibTests extends GroovyPagesTestCase {
void testDateFormat() {
def template = '<g:dateFormat format="dd-MM-yyyy" date="${myDate}" />' def testDate = … // create the date
assertOutputEquals( '01-01-2008', template, [myDate:testDate] )
}
}
또 GroovyPagesTestCase 클래스의 applyTemplate 메소드를 사용하여 GSP의 결과를 얻을 수 있다:class FormatTagLibTests extends GroovyPagesTestCase {
void testDateFormat() {
def template = '<g:dateFormat format="dd-MM-yyyy" date="${myDate}" />' def testDate = … // create the date
def result = applyTemplate( template, [myDate:testDate] ) assertEquals '01-01-2008', result
}
}
Testing Domain Classes(도메인 클래스 테스트하기)
보통 도메인 클래스를 테스트하는 것은 GORM API의 문제이지만 먼저 알아야 하는 것들이 있다. 만약 쿼리를 테스트하고 있다면 데이터베이스에 저장돼 올바른 상태가 보장되도록 종종 “플러쉬(flush)“시켜야만 한다. 예를 들어 다음과 같은 예제가 있다면:
void testQuery() {
def books = [ new Book(title:"The Stand"), new Book(title:"The Shining")]
books*.save() assertEquals 2, Book.list().size()
}
이 테스트는 save 메소드가 호출되면 바로 Book 인스턴스는 저장되지 않으므로 실패할 것이다. save를 호출하는 것은 단지 하이버네이트가 언젠가 인스턴스를 데이터베이스에 저장해야 한다는 것을 알리는 것뿐이다. 만약 즉시 저장되게 하고 싶다면 “플러쉬”시켜야 한다.void testQuery() {
def books = [ new Book(title:"The Stand"), new Book(title:"The Shining")]
books*.save(flush:true) assertEquals 2, Book.list().size()
}
이 예제는 flush 인자의 값을 true로 넘기기 때문에 즉시 데이터베이스에 저장된다. 그래서 이제 데이터베이스에 질의해도 된다.
기능 테스트는 어플리케이션을 실행시키는 테스트이고 보통 자동화하기 어렵다. Grails는 기본적으로 기능 테스트를 지원하지 않는다. 하지만 Canoo 웹 테스트 플러그인 이 있다.웹 테스트를 설치하기 위해서 다음과 같은 명령을 실행한다:grails install-plugin webtest
웹 테스트와 Grails를 함께 사용하는 방법은 위키를 참조해야 한다.